package com.google.gson;

import com.google.gson.internal.Primitives;
import com.google.gson.internal.bind.ArrayTypeAdapter;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.*;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class Gson {

    private static final String JSON_NULL = "null";

    private List<TypeAdapterFactory> factories;

    public Gson() {
        addFactories();
    }

    /**
     * This method serializes the specified object into its equivalent JSON representation.
     * This method should be used when the specified object is not a generic type. This method uses
     * {@link Class#getClass()} to get the type for the specified object, but the
     * {@code getClass()} loses the generic type information because of the Type Erasure feature
     * of Java. Note that this method works fine if any of the object fields are of generic type,
     * just the object itself should not be of a generic type. If the object is of generic type, use
     * {@link #toJson(Object, Type)} instead. If you want to write out the object to a
     * {@link Writer}, use {@link #toJson(Object, Appendable)} instead.
     *
     * @param src the object for which JSON representation is to be created
     * @return Json representation of {@code src}.
     *
     * @see #toJson(Object, Appendable)
     * @see #toJson(Object, Type)
     */
    public String toJson(Object src) {
        if (src == null) {
            return JSON_NULL;
        }
        return toJson(src, src.getClass());
    }

    /**
     * This method serializes the specified object, including those of generic types, into its
     * equivalent JSON representation. This method must be used if the specified object is a generic
     * type. For non-generic objects, use {@link #toJson(Object)} instead. If you want to write out
     * the object to a {@link Appendable}, use {@link #toJson(Object, Type, Appendable)} instead.
     *
     * @param src the object for which JSON representation is to be created
     * @param typeOfSrc The specific genericized type of src. You can obtain
     * this type by using the {@link com.google.gson.reflect.TypeToken} class. For example,
     * to get the type for {@code Collection<Foo>}, you should use:
     * <pre>
     * Type typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
     * </pre>
     * @return JSON representation of {@code src}
     *
     * @see #toJson(Object, Type, Appendable)
     * @see #toJson(Object)
     */
    public String toJson(Object src, Type typeOfSrc) {
        StringWriter writer = new StringWriter();
        toJson(src, typeOfSrc, writer);
        return writer.toString();
    }

    /**
     * This method serializes the specified object into its equivalent JSON representation and
     * writes it to the writer.
     * This method should be used when the specified object is not a generic type. This method uses
     * {@link Class#getClass()} to get the type for the specified object, but the
     * {@code getClass()} loses the generic type information because of the Type Erasure feature
     * of Java. Note that this method works fine if any of the object fields are of generic type,
     * just the object itself should not be of a generic type. If the object is of generic type, use
     * {@link #toJson(Object, Type, Appendable)} instead.
     *
     * @param src the object for which JSON representation is to be created
     * @param writer Writer to which the JSON representation needs to be written
     * @throws JsonIOException if there was a problem writing to the writer
     * @since 1.2
     *
     * @see #toJson(Object)
     * @see #toJson(Object, Type, Appendable)
     */
    public void toJson(Object src, Writer writer) throws JsonException {
        if (src != null) {
            toJson(src, src.getClass(), writer);
        }
    }

    /**
     * This method serializes the specified object, including those of generic types, into its
     * equivalent JSON representation and writes it to the writer.
     * This method must be used if the specified object is a generic type. For non-generic objects,
     * use {@link #toJson(Object, Appendable)} instead.
     *
     * @param src the object for which JSON representation is to be created
     * @param typeOfSrc The specific genericized type of src. You can obtain
     * this type by using the {@link com.google.gson.reflect.TypeToken} class. For example,
     * to get the type for {@code Collection<Foo>}, you should use:
     * <pre>
     * Type typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
     * </pre>
     * @param writer Writer to which the JSON representation of src needs to be written.
     * @throws JsonIOException if there was a problem writing to the writer
     * @since 1.2
     *
     * @see #toJson(Object, Type)
     * @see #toJson(Object, Appendable)
     */
    public void toJson(Object src, Type typeOfSrc, Writer writer) throws JsonException {
        JsonWriter jsonWriter = newJsonWriter(writer);
        toJson(src, typeOfSrc, jsonWriter);
    }

    /**
     * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to
     * {@code writer}.
     *
     * <p>The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode},
     * regardless of the lenient mode setting of the provided writer. The lenient mode setting
     * of the writer is restored once this method returns.
     *
     * <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
     * (configured by the {@link GsonBuilder}) are applied, and the original settings of the
     * writer are restored once this method returns.
     *
     * @throws JsonIOException if there was a problem writing to the writer
     */
    public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonException {
        TypeAdapter<Object> adapter = (TypeAdapter<Object>) getAdapter(TypeToken.get(typeOfSrc));
        try {
            adapter.write(writer, src);
        } catch (IOException e) {
            throw new JsonException(e);
        }
    }

    public JsonWriter newJsonWriter(Writer writer) {
        JsonWriter jsonWriter = new JsonWriter(writer);

        return jsonWriter;
    }


    /**
     * This method deserializes the specified JSON into an object of the specified class. It is not
     * suitable to use if the specified class is a generic type since it will not have the generic
     * type information because of the Type Erasure feature of Java. Therefore, this method should not
     * be used if the desired type is a generic type. Note that this method works fine if the any of
     * the fields of the specified object are generics, just the object itself should not be a
     * generic type. For the cases when the object is of generic type, invoke
     * {@link #fromJson(String, TypeToken)}. If you have the JSON in a {@link Reader} instead of
     * a String, use {@link #fromJson(Reader, Class)} instead.
     *
     * <p>An exception is thrown if the JSON string has multiple top-level JSON elements, or if there
     * is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is not desired.
     *
     * @param <T> the type of the desired object
     * @param json the string from which the object is to be deserialized
     * @param classOfT the class of T
     * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}
     * or if {@code json} is empty.
     * @throws JsonSyntaxException if json is not a valid representation for an object of type
     * classOfT
     *
     * @see #fromJson(Reader, Class)
     * @see #fromJson(String, TypeToken)
     */
    public <T> T fromJson(String json, Class<T> classOfT) throws JsonException {
        T object = fromJson(json, TypeToken.get(classOfT));
        return Primitives.wrap(classOfT).cast(object);
    }

    public <T> T fromJson(String json, Type typeOfT) throws JsonException {
        return (T) fromJson(json, TypeToken.get(typeOfT));
    }


    public <T> T fromJson(String json, TypeToken<T> typeOfT) throws JsonException {
        if (json == null) {
            return null;
        }
        StringReader reader = new StringReader(json);
        return fromJson(reader, typeOfT);
    }


    public <T> T fromJson(Reader json, TypeToken<T> typeOfT) throws JsonException {
        JsonReader jsonReader = newJsonReader(json);
        T object = fromJson(jsonReader, typeOfT);
        assertFullConsumption(object, jsonReader);
        return object;
    }

    private static void assertFullConsumption(Object obj, JsonReader reader) {
        try {
            if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) {
                throw new JsonException("JSON document was not fully consumed.");
            }
        } catch (IOException e) {
            throw new JsonException("read buffer error");
        }
    }

    public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) throws JsonException {
        try {
            reader.peek();
            TypeAdapter<T> typeAdapter = getAdapter(typeOfT);
            T object = typeAdapter.read(reader);
            return object;
        }  catch (Exception e) {
            throw new JsonException("Failed to deserialize to object from the class " + typeOfT.getRawType().getName(), e);
        }
    }

    /**
     * Returns the type adapter for {@code} type.
     *
     * @throws IllegalArgumentException if this GSON cannot serialize and
     *     deserialize {@code type}.
     */
    public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
        Objects.requireNonNull(type, "type must not be null");

        for (TypeAdapterFactory factory : factories) {
            TypeAdapter<T> candidate = factory.create(this, type);
            if (candidate != null) {
                return candidate;
            }
        }
        throw new JsonException("GSON cannot handle " + type);
    }


    /**
     * Returns a new JSON reader configured for the settings on this Gson instance.
     */
    public JsonReader newJsonReader(Reader reader) {
        JsonReader jsonReader = new JsonReader(reader);
        return jsonReader;
    }

    private void addFactories() {
        List<TypeAdapterFactory> factories = new ArrayList<>();

        // type adapters for basic platform types
        factories.add(TypeAdapters.STRING_FACTORY);
        factories.add(TypeAdapters.INTEGER_FACTORY);
        factories.add(TypeAdapters.BOOLEAN_FACTORY);

        factories.add(TypeAdapters.DOUBLE_FACTORY);

        factories.add(ArrayTypeAdapter.FACTORY);

        factories.add(new ReflectiveTypeAdapterFactory());

        this.factories = Collections.unmodifiableList(factories);
    }
}
